Skip to content

docs: timers concept page#75

Merged
marc0olo merged 2 commits into
mainfrom
docs/concepts-timers
Apr 16, 2026
Merged

docs: timers concept page#75
marc0olo merged 2 commits into
mainfrom
docs/concepts-timers

Conversation

@marc0olo
Copy link
Copy Markdown
Member

Summary

  • Explains the protocol-level global timer: one timer per canister, ic0.global_timer_set() system API, canister_global_timer handler
  • Covers how CDK timer libraries (ic-cdk-timers in Rust, mo:core/Timer in Motoko) build multiple/periodic timers on top via self-canister-calls
  • Explains one-shot vs recurring timers, TimerId cancellation, scheduling guarantees (best-effort, not real-time)
  • Upgrade behavior (timers do not persist), re-registration in postupgrade
  • Timers vs heartbeats comparison table; security considerations (vanishing on upgrades, reentrancy at await)

Sync recommendation

informed by dfinity/portal — periodic-tasks-timers.mdx; dfinity/cdk-rs — ic-cdk-timers/src/lib.rs; caffeinelabs/motoko — doc/md/core/Timer.md

@marc0olo
Copy link
Copy Markdown
Member Author

Review: Timers

Must fix

  • Rescheduling timing description is inaccurate: The page says "The library reschedules them automatically at the end of each execution." This is incorrect. Per ic-cdk-timers/src/global_timer.rs (lines 211–216), recurring timers are rescheduled when the self-call is dispatched (i.e., when call_perform succeeds in the same canister_global_timer invocation), not after execution completes. The reschedule base is timer_scheduled_time — meaning the next fire time is measured from when the timer was originally scheduled to fire, not from when the callback finishes. This distinction is observable: a 5-second recurring timer with a 2-second callback will fire at 5s, 10s, 15s (not 5s, 12s, 19s). Suggested fix for the "One-shot vs. recurring timers" section: change "The library reschedules them automatically at the end of each execution" to "The library reschedules them when the self-call is dispatched — the next interval is measured from the originally-scheduled fire time, not from when the callback finishes."

Suggestions

  • Frontmatter description is too narrow: The description reads "How the IC schedules periodic and one-shot tasks via the global timer mechanism." The page covers substantially more: CDK libraries, scheduling guarantees, upgrade behavior, heartbeat comparison, and security considerations. Suggested: "How canisters schedule automatic work: the global timer, CDK timer libraries, scheduling guarantees, upgrade behavior, and security considerations."

  • set_timer_interval_serial skip behavior not prominent enough: The page mentions set_timer_interval_serial only once, in the security section, as a reentrancy mitigation. Developers choosing between set_timer_interval and set_timer_interval_serial need to know the key behavioral difference: with set_timer_interval_serial, if the previous invocation is still running when the next one is due, the new invocation is silently skipped (not delayed). This is verified in global_timer.rs (lines 129–132: RepeatedSerialBusy causes reschedule-and-skip). This behavioral difference belongs in the "One-shot vs. recurring timers" section, alongside the description of recurring timers.

  • Heartbeat sub-second claim overstates practical difference: The page says "For sub-second precision, heartbeats provide finer granularity." The portal source describes the heartbeat interval as "close to the blockchain finalization rate (~1s)." Both timers and heartbeats fire at approximately 1-second granularity — heartbeats fire on every round, which is roughly the same cadence as the minimum useful timer interval. Consider removing the sub-second framing and instead focusing on the actual difference: heartbeats fire unconditionally every round regardless of whether there's work to do, while timers only fire when scheduled.

  • Missing: cycles cost when insufficient liquid cycles: The global_timer.rs source (lines 152–158) shows the library checks ic0::canister_liquid_cycle_balance128() before each self-call and will skip the timer if there aren't enough liquid cycles, logging "unable to schedule timer: not enough liquid cycles". This is an observable failure mode not mentioned in the "Scheduling guarantees" section. Developers running canisters at low cycle balances may see silent timer misses. Adding a brief note here would improve the page's completeness.

Verified

  • All internal links resolve: ../reference/ic-interface-spec.mddocs/reference/ic-interface-spec.md exists; canisters.mddocs/concepts/canisters.md exists; ../guides/backends/timers.mddocs/guides/backends/timers.mdx exists (Astro resolves .md extension to .mdx files per CLAUDE.md).
  • External URL format: https://docs.rs/ic-cdk-timers/latest/ic_cdk_timers/ matches the correct docs.rs pattern per content-authoring.md linking rules (hyphens in crate name, underscores in path).
  • Rust API names verified against .sources/cdk-rs/ic-cdk-timers/src/lib.rs: set_timer, set_timer_interval, set_timer_interval_serial, and clear_timer are all correct public exports. TimerId is the correct return type (re-exported from state::TaskId).
  • Motoko API names verified against .sources/motoko-core/src/Timer.mo: Timer.setTimer, Timer.recurringTimer, and Timer.cancelTimer are all correct public functions in mo:core/Timer.
  • Priority queue claim accurate: The library uses BinaryHeap<Timer> (state.rs line 18). Described as a priority queue in the page — correct.
  • Self-canister call isolation confirmed: timer_executor.rs implements a canister_update export (<ic-cdk internal> timer_executor) invoked via call_perform self-call. The page's description of task isolation via self-canister calls is accurate.
  • Output queue 500 message limit confirmed: Portal source periodic-tasks-timers.mdx line 98 explicitly states "the canister output queue is limited in size (500 messages)." The page's claim is accurate.
  • Timers not persisted across upgrades: Confirmed in lib.rs module-level doc and per-function #[doc] attributes. The page accurately describes this.
  • set_timer_interval_serial skip behavior confirmed: RepeatedSerialBusy state in global_timer.rs (lines 129–132) confirms invocations are rescheduled and skipped (not queued) when the previous one is still running. Page's security section description is accurate.
  • No banned patterns: No dfx references, no mo:base imports, no internetcomputer.org/docs/ links, no docs.internetcomputer.org links.
  • Frontmatter completeness: title and description present. sidebar.order: 8 present. No contradictions between frontmatter and body content.
  • Upstream comment present: <!-- Upstream: informed by dfinity/portal docs/building-apps/network-features/periodic-tasks-timers.mdx, dfinity/cdk-rs ic-cdk-timers/src/lib.rs, caffeinelabs/motoko doc/md/core/Timer.md --> at end of file — correct format.
  • Diataxis compliance: Page is in concepts/ and follows the Explanation pattern — no CLI commands, no step-by-step procedures. Heartbeats comparison table is appropriate for a concept page.
  • Next steps section present and correct: Links to guides/backends/timers.md, concepts/canisters.md, and reference/ic-interface-spec.md. All targets exist.
  • Content brief coverage: Migration plan task 61 specifies "Portal periodic-tasks conceptual parts." The page covers all major conceptual aspects: global timer protocol, CDK libraries, scheduling guarantees, upgrade behavior, and heartbeat comparison. Coverage is comprehensive.
  • Reader experience: Opens with a clear statement of what the page is about. Does not assume prior context beyond general canister knowledge. Sections flow logically: protocol → libraries → behavior → guarantees → upgrades → comparison → security → next steps.

- Fix rescheduling description: recurring timers are rescheduled at
  dispatch time using the originally-scheduled fire time as base, not
  at callback completion time (verified in cdk-rs global_timer.rs)
- Add set_timer_interval vs set_timer_interval_serial comparison with
  skip behavior for serial timers when previous invocation is still running
- Add cycles check failure mode to Scheduling guarantees section
- Update heartbeats section: remove misleading sub-second precision claim
  since both timers and heartbeats operate at ~1s block rate
- Update frontmatter description to reflect full page scope
@marc0olo
Copy link
Copy Markdown
Member Author

<!-- feedback-addressed -->
Feedback addressed:

Changes applied

Must fix:

  • Rescheduling timing description corrected (One-shot vs. recurring timers section): Changed "The library reschedules them automatically at the end of each execution" to accurately describe that reschedule happens when the self-call is dispatched (when call_perform succeeds), and that the next interval is measured from the originally-scheduled fire time — not from when the callback finishes. Added a concrete example (5s timer with 2s callback fires at 5s/10s/15s, not 5s/12s/19s). Verified against cdk-rs/ic-cdk-timers/src/global_timer.rs lines 207–215: reschedule_timer(timers, task_id, timer_scheduled_time, *interval) is called inside the errcode == 0 branch (dispatch success), using timer_scheduled_time as the base.

Suggestions applied:

  • Frontmatter description updated: Changed from "How the IC schedules periodic and one-shot tasks via the global timer mechanism" to "How canisters schedule automatic work: the global timer, CDK timer libraries, scheduling guarantees, upgrade behavior, and security considerations." to reflect the full page scope.

  • set_timer_interval_serial skip behavior added (One-shot vs. recurring timers section): Added a comparison of set_timer_interval (allows up to 5 concurrent invocations) vs set_timer_interval_serial (strict serial — silently skips new invocations when previous one is still running). Verified against global_timer.rs lines 129–132: RepeatedSerialBusy state reschedules using timer_scheduled_time and skips. Also updated the security section to note the skip behavior explicitly when referencing set_timer_interval_serial.

  • Cycles check failure mode added (Scheduling guarantees section): Added bullet point: "If the canister has insufficient liquid cycles when a timer is due to fire, the library will skip that invocation and log unable to schedule timer: not enough liquid cycles. Canisters running at low cycle balances can experience silent timer misses." Verified against global_timer.rs lines 152–157: canister_liquid_cycle_balance128() check with the exact log message.

  • Heartbeat sub-second claim removed (Timers vs. heartbeats section): Removed the sentence "For sub-second precision, heartbeats provide finer granularity at the cost of the limitations above." Replaced the closing paragraph with: "Both timers and heartbeats operate at approximately the block rate (~1 second), so heartbeats do not provide finer time resolution than timers." Updated the guidance to focus on the actual behavioral difference (heartbeats fire unconditionally every round).

Items skipped

None — all four feedback items were verified correct against .sources/cdk-rs/ic-cdk-timers/src/ and applied.

@marc0olo marc0olo merged commit 239074f into main Apr 16, 2026
1 check passed
@marc0olo marc0olo deleted the docs/concepts-timers branch April 16, 2026 15:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant